---
title: 高级发布机制
slug: advanced-publications
date: 0013/01/02
number: 13.5
points: 10
sidebar: true
photoUrl: http://www.flickr.com/photos/ikewinski/8390558986/
photoAuthor: Mike Lewinski
contents: 学习更多处理发布机制的高级方法。|了解发布/订阅模型可以得到怎样的弹性机制。
paragraphs: 36
---

目前你应该对发布和订阅交互模式有一个不错的掌握了。因此，我们废话少说，来看几个更高级的情景。

### 多次发布一个集合

在[我们第一个关于发布的附录中](/chapter/publications-and-subscriptions/)，我们看到了一些更普遍的发布和订阅模式，同时我们学习了 `_publishCursor` 函数，如何让它们非常容易地实现在我们的站点上。

首先，让我们回忆 `_publishCursor` 到底为我们做了什么：它将整理所有的文档以匹配一个给定的游标（cursor），并将它们推送至*同名的*客户端集合中。注意这与 _publication_ 的名字是不关联的。

这意味着我们可以用_不止一个 publicaton_ 去连接任何集合的客户端与服务端版本。

我们已经在[分页章节](/chapter/pagination/)用过这个模式，当我们在当前显示的帖子之外，再发布一个所有帖子的分页后的子集。

另一个相似的用例是发布一大组文档的*预览*，和单个文档的全部信息：

<%= diagram "doublecollection", "两次发布一个集合", "pull-center" %>

~~~js
Meteor.publish('allPosts', function() {
  return Posts.find({}, {fields: {title: true, author: true}});
});

Meteor.publish('postDetail', function(postId) {
  return Posts.find(postId);
});
~~~

现在客户端订阅这两个发布，这 `'posts'` 集合来自于两个源渠道：来自第一个订阅的标题和作者姓名列表，和来自第二个订阅的单个帖子全部信息。

你也许意识到了 `postDetail` 发布的帖子也被 `allPosts` 发布了（尽管只有它的部分属性）。但是，Meteor 会合并字段及确认没有重复的帖子，来处理数据重叠的问题。

这是很棒的，因为现在当我们呈现帖子摘要列表时，我们正在处理的数据对象正好拥有我们需要显示的足够数据。但是，当我们呈现单个帖子时，我们有一切需要展示的数据。当然，在这种情况下，我们需要让客户端不要去期待所有帖子的所有字段都能都显示出来————这是一个常见的问题！

注意你并没有改变文档属性的任何限制。你可以很好地在这两个发布中发布同样的属性，但是先后排序不同。

~~~js
Meteor.publish('newPosts', function(limit) {
  return Posts.find({}, {sort: {submitted: -1}, limit: limit});
});

Meteor.publish('bestPosts', function(limit) {
  return Posts.find({}, {sort: {votes: -1, submitted: -1}, limit: limit});
});
~~~
<%= caption "server/publications.js" %>

### 多次订阅一个发布

我们已经看了如何多次发布同一个集合。事实证明你可以通过另一个模式来完成非常相近的结果：建立一个单一发布，却多次*订阅*它。

在 Microscope 中，我们多次重复订阅 `posts` 发布，但 Iron Router 为我们设置并拆开每次的订阅。然而，没有理由我们不能*同时进行*多次订阅。

举个例子，我们想要将最新的和最好的帖子同时载入内存：

<%= diagram "subscribetwice", "对一个发布订阅两次", "pull-center" %>

我们设定一个单一发布：

~~~js
Meteor.publish('posts', function(options) {
  return Posts.find({}, options);
});
~~~

并且我们多次订阅这个发布。事实上或多或少我们在 Microscope 里这样做了：

~~~js
Meteor.subscribe('posts', {submitted: -1, limit: 10});
Meteor.subscribe('posts', {baseScore: -1, submitted: -1, limit: 10});
~~~

接下来到底发生什么了？每个浏览器开启了*两个*不同的订阅，每个订阅连接到*同个*服务端的发布。

每个订阅提供了不同的发布参数，但从根本上，每次一个（不同）文档子集从 `posts` 集合提取出来，并通过连接机制发送到客户端集合。

你甚至可以用*同样的参数*订阅两次相同的发布。这个对很多处场景来说很难说有用，但这种弹性机制总有一天会有用的。

### 单一订阅中的多个集合

不像传统关系型数据库像 MySQL 使用 *joins*，NoSQL 数据库类似 Mongo 都是关于*去规范化*和*嵌入*。让我们看看它们是怎样在 Meteor 环境下工作的。

让我们看一个具体的例子。我们已经对我们的帖子添加了评论，到目前为止，我们一直很愉快地只发布用户看的单个帖子的评论。

但是，假设我们希望在首页中显示*全部*帖子的回复（记着这些帖子会在分页时被改变）。这个用例展示了一个很好的理由把评论嵌入帖子中，事实上这是促使我们来非规范化评论*数量*。

当然我们可以总是嵌入评论到帖子中，并完全摒除 `Comments` 集合。但如同我们前面在*去规范化*章节看到的，我们将在分离的集合的操作中也会失去一些额外的好处。

但是事实证明有一个涉及订阅的技巧，在保持分离集合的同时再嵌入我们的评论。

让我们假定除了首页帖子列表之外，我们希望再订阅每个帖子的两个最新评论。

使用独立的评论发布会很难完成这个要求，尤其在帖子列表受到某些限制时（比如说，最近的10个）。我们必须写一个发布，看起来像下面的代码：

<%= diagram "multiplecollections", "一个订阅中的两个集合", "pull-center" %>

~~~js
Meteor.publish('topComments', function(topPostIds) {
  return Comments.find({postId: topPostIds});
});
~~~

从性能角度来看这是个问题，因为这个发布将需要每次在 `topPostIds` 改变时消除及重新建立。

有一个途径来解决这个问题。我们可以应用这个事实：就是我们不仅可以在每个*集合*上拥有多次*发布*，而且我们也可以在每个*发布*上拥有多个*集合*。

~~~js
Meteor.publish('topPosts', function(limit) {
  var sub = this, commentHandles = [], postHandle = null;

  // send over the top two comments attached to a single post
  function publishPostComments(postId) {
    var commentsCursor = Comments.find({postId: postId}, {limit: 2});
    commentHandles[postId] =
      Mongo.Collection._publishCursor(commentsCursor, sub, 'comments');
  }

  postHandle = Posts.find({}, {limit: limit}).observeChanges({
    added: function(id, post) {
      publishPostComments(id);
      sub.added('posts', id, post);
    },
    changed: function(id, fields) {
      sub.changed('posts', id, fields);
    },
    removed: function(id) {
      // stop observing changes on the post's comments
      commentHandles[id] && commentHandles[id].stop();
      // delete the post
      sub.removed('posts', id);
    }
  });

  sub.ready();

  // make sure we clean everything up (note `_publishCursor`
  //   does this for us with the comment observers)
  sub.onStop(function() { postHandle.stop(); });
});
~~~

注意我们在这个发布中没有返回任何东西，因为我们自己手动给 `sub` 发送信息（通过 `.added()` 等方式）。所以我们不必通过返回一个游标来请求 `_publishCursor` 给我们来做这个动作。

现在，每次我们发布一个帖子时，我们也自动发布其2个最新评论。而且所有都在一个订阅调用中！

虽然 Meteor 还未直接实现这个方法，但是你也可以参考在 Atomsphere 里的 `publish-with-relations` 包，它的目标就是让这个模式更容易使用。

### 连接不同的集合

这样的订阅弹性机制还能给我们更多新知识么？当然，如果我们不使用 `_publishCursor`，我们不必跟着此项约束，就是在服务端的源集合需要与客户端的目标集合有同样的名称。

<%= diagram "linkedcollections", "一个集合对两个订阅", "pull-center" %>

为什么我们想这么做的一个原因就是*单表继承*。

假设我们需要从我们的帖子中引用多种类型的对象，每一个对象存储在相同字段中但又显然是不同内容。例如，我们建立一个类似 Tumblr 的博客引擎，每个帖子具有常见的 ID、时间戳，以及标题；但是额外也有如图像、视频、链接，或者只是文字。

我们可以将这些对象存储在一个单独的 `'resources'` 集合中，使用 `type` 属性来标记他们是什么类型的对象（`video`、`image`、`link` 等等）。

同时，虽然我们有一个服务端的单一的 `Resources` 集合，我们也能够将单一集合转换到多个的 `Videos`、`Images'，等等集合中。
客户端的集合如下的代码：

~~~js
  Meteor.publish('videos', function() {
    var sub = this;

    var videosCursor = Resources.find({type: 'video'});
    Mongo.Collection._publishCursor(videosCursor, sub, 'videos');

    // _publishCursor doesn't call this for us in case we do this more than once.
    sub.ready();
  });
~~~

我们告诉 `_publishCursor` (就像返回）游标会做的一样发布我们的视频，但不是将 `resources` 集合发布到客户端，而是我们从 `resources` 发布到 `videos`。

另一个类似的主意是：发布到客户端的集合，却根本*没有服务端集合*！举例，你也许从一个第三方服务中抓取数据，并发布它们到一个客户端集合。

由于发布 API 的灵活性，可能性是无限的。
